package cn.xpp011.distributedlocksdemo.aop.locks;

import cn.xpp011.distributedlocksdemo.annotation.locks.DistributedLock;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @program: distributed-locks-demo
 * @description:
 * @author: xpp011
 * @create: 2022-03-05 22:41
 **/
@Aspect
@Component
public class LocksAspect {

    private static final Logger log = LoggerFactory.getLogger(LocksAspect.class);

    private static final double KEY_TIMEOUT_OFFSET = 0.75;

    private static final String suffix = "-locks";


    /**
     * 该线程池核心线程数为1
     * 最大线程数为Integer.MAX_VALUE;
     * 空闲线程最大活跃时间10000000纳秒
     */
    private static final ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();

    private static final Map<String, ScheduledFuture<?>> scheduledFutureMap = new ConcurrentHashMap<>();

    private static final Random random = new Random();

    static {
        threadPoolTaskScheduler.initialize();
    }

    @Autowired
    private RedisTemplate redisTemplate;

    @Around(value = "@annotation(lock)", argNames = "joinPoint,lock")
    void around(ProceedingJoinPoint joinPoint, DistributedLock lock) {
        StringBuilder str = new StringBuilder(lock.value());
        if (lock.paramLock()){
            //参数参与锁的构建
            Object[] args = joinPoint.getArgs();
            for (Object arg : args) {
                if (arg == null) continue;
                str.append("-" + arg.hashCode());
            }
        }
        //依据参数实现锁
        String key = new StringBuilder()
                .append(lock.prefix()).append(str).append(suffix)
                .toString();
        //重试次数
        int retryCut = lock.retryCount(), sub = retryCut == 0 ? 0 : 1;
        //重试间隔
        long retryInterval = lock.retryInterval(), keyTimeout = lock.keyTimeout();

        //锁的value
        String value = UUID.randomUUID().toString();

        verify(retryCut, retryInterval, key);

        try {
            while ((retryCut -= sub) >= 0) {

                Boolean absent = redisTemplate.opsForValue().setIfAbsent(key, value, keyTimeout, TimeUnit.MILLISECONDS);
                if (absent) {
                    //拿到锁,开启看门狗进行锁续期
                    startWatchDog(key, value, keyTimeout);

                    //执行业务代码
                    joinPoint.proceed();

                    break;
                } else {
                    //未拿到锁,停顿尝试重新获取
                    log.info("{}  key:{} 未拿到锁，准备重试", Thread.currentThread().getName(), key);
                    Thread.sleep((long) (retryInterval + retryInterval * random.nextDouble()));
                }
            }
        } catch (InterruptedException e) {
            log.error("线程睡眠被打断");
            e.printStackTrace();
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            //删除锁, 非原子操作调用lua脚本
            if (delLock(key, value)) {
                //取消看门狗
                removeWatchDog(key);
            }
        }
    }

    /**
     * 调用lua脚本删除锁
     *
     * @param key
     * @param value
     */
    private boolean delLock(String key, String value) {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("locks/del-lock.lua")));
        redisScript.setResultType(Long.class);
        Long execute = (Long) redisTemplate.execute(redisScript, Arrays.asList(key), value);
        log.info("{}  key:{} 删除{}",Thread.currentThread().getName(), key, execute == 0L ? "失败" : "成功");
        return execute != 0L;
    }

    /**
     * 检验参数合法性
     *
     * @param retryCut
     * @param retryInterval
     * @param key
     */
    private void verify(int retryCut, double retryInterval, String key) {
        StringBuilder errMsg = new StringBuilder();
        if (retryCut < 0) errMsg.append("retryCut ");
        if (retryInterval <= 0) errMsg.append("retryInterval ");
        int lio = key.lastIndexOf(suffix);
        if (lio == -1 || lio != key.length() - suffix.length()) errMsg.append("key ");
        if (errMsg.length() != 0) throw new RuntimeException(errMsg.append("参数不合法").toString());
    }

    /**
     * 看门狗
     * 添加守护线程实现锁续期
     * 看门狗这一块还没想到有什么好的方式实现，待我变强后来重构
     *
     * @param key
     * @param keyTimeout
     */
    private void startWatchDog(String key, String value, long keyTimeout) {
        AtomicInteger i = new AtomicInteger();
        ScheduledFuture<?> future = threadPoolTaskScheduler.scheduleAtFixedRate(() -> {
            //锁续期
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("locks/pexpire-lock.lua")));
            redisScript.setResultType(Long.class);
            Long execute = (Long) redisTemplate.execute(redisScript, Arrays.asList(key), value, keyTimeout);
            //续期失败删除看门狗
            if (execute == 0L) removeWatchDog(key);
            //锁续期失败
            log.info("{} key:{}，第{}锁续期{}",Thread.currentThread().getName(), key, i.incrementAndGet(), execute == 0L ? "失败" : "成功");
        }, (long) (keyTimeout * KEY_TIMEOUT_OFFSET));

        scheduledFutureMap.put(key, future);
    }

    /**
     * 取消看门狗定时任务
     * @param key
     */
    private void removeWatchDog(@Nullable String key){
        ScheduledFuture<?> future = scheduledFutureMap.get(key);
        if (future != null && !future.isCancelled()) future.cancel(false);
        scheduledFutureMap.remove(key);
        log.info("取消看门狗 key:{}", key);
    }
}
